package mage.client.util.audio;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineEvent.Type;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import mage.utils.ThreadUtils;
public class LinePool {
private final Logger log = LoggerFactory.getLogger(getClass());
private static final int LINE_CLEANUP_INTERVAL = 30000;
private final Queue<SourceDataLine> freeLines = new ArrayDeque<>();
private final Queue<SourceDataLine> activeLines = new ArrayDeque<>();
private final Set<SourceDataLine> busyLines = new HashSet<>();
private final LinkedList<MageClip> queue = new LinkedList<>();
/*
* Initially all the lines are in the freeLines pool. When a sound plays, one line is being selected randomly from
* the activeLines and then, if it's empty, from the freeLines pool and used to play the sound. The line is moved to
* busyLines. When a sound stops, the line is moved to activeLines if it contains <= elements than alwaysActive
* parameter, else it's moved to the freeLines pool. Every 30 seconds the lines in the freeLines pool are closed
* from the timer thread to prevent deadlocks in PulseAudio internals.
*/
private final Mixer mixer;
private final int alwaysActive;
public LinePool() {
this(new AudioFormat(22050, 16, 1, true, false), 4, 1);
}
public LinePool(AudioFormat audioFormat, int size, int alwaysActive) {
this.alwaysActive = alwaysActive;
mixer = AudioSystem.getMixer(null);
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, audioFormat);
for (int i = 0; i < size; i++) {
try {
SourceDataLine line = (SourceDataLine) mixer.getLine(lineInfo);
freeLines.add(line);
} catch (LineUnavailableException e) {
log.warn("Failed to get line from mixer", e);
}
}
new Timer("Line cleanup", true).scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
synchronized (LinePool.this) {
for (SourceDataLine sourceDataLine : freeLines) {
if (sourceDataLine.isOpen()) {
sourceDataLine.close();
log.debug("Closed line {}", sourceDataLine);
}
}
}
}
}, LINE_CLEANUP_INTERVAL, LINE_CLEANUP_INTERVAL);
}
private synchronized SourceDataLine borrowLine() {
SourceDataLine line = activeLines.poll();
if (line == null) {
line = freeLines.poll();
}
if (line != null) {
busyLines.add(line);
}
return line;
}
private synchronized void returnLine(SourceDataLine line) {
busyLines.remove(line);
if (activeLines.size() < alwaysActive) {
activeLines.add(line);
} else {
freeLines.add(line);
}
}
public void playSound(final MageClip mageClip) {
final SourceDataLine line;
synchronized (LinePool.this) {
log.debug("Playing {}", mageClip.getFilename());
logLineStats();
line = borrowLine();
if (line == null) {
// no lines available, queue sound to play it when a line is available
queue.add(mageClip);
log.debug("Sound {} queued.", mageClip.getFilename());
return;
}
logLineStats();
}
ThreadUtils.threadPool.submit(() -> {
synchronized (LinePool.this) {
try {
if (!line.isOpen()) {
line.open();
line.addLineListener(event -> {
log.debug("Event: {}", event);
if (event.getType() != Type.STOP) {
return;
}
synchronized (LinePool.this) {
log.debug("Before stop on line {}", line);
logLineStats();
returnLine(line);
log.debug("After stop on line {}", line);
logLineStats();
MageClip queuedSound = queue.poll();
if (queuedSound != null) {
log.debug("Playing queued sound {}", queuedSound);
playSound(queuedSound);
}
}
});
}
line.start();
} catch (LineUnavailableException e) {
log.warn("Failed to open line", e);
}
}
byte[] buffer = mageClip.getBuffer();
log.debug("Before write to line {}", line);
line.write(buffer, 0, buffer.length);
line.drain();
line.stop();
log.debug("Line completed: {}", line);
});
}
private void logLineStats() {
log.debug("Free lines: {} Active: {} Busy: {}", freeLines.size(), activeLines.size(), busyLines.size());
}
}